/*
Plan
 1. order by
 2. join

2012-07-26：
 1. 刪除 generateTableCache 方法內多印一次 SQL 語法的程式碼。

2012-06-26：
 1. PreparedStatement and ResultSet 使用完畢還是要自己關，不可以靠 Connection 最後來收尾！
    excuteModify, generateTableCache.

2011-02-22：
 1. 變更 runDesignate, printSQL method 從 private 開放為 protected 讓繼承後可以修改。

2011-01-20：
 1. 增加 select 方法組 startSelect(), appendSelect(Object), endSelect()。

2008-03-19：
 1. 修改使用 DatabaseAdapter 執行 SQL 的方法〈excuteInsert, excuteModify, excuteUpdate,
    generateTableCache〉增設檢測如果發生 SQLException 的時候要 freeConnection 後再把 SQLException 丟出。
*/
package idv.jieh.commons.sql;

import java.sql.*;
import java.util.*;

public class SQLMaker extends idv.jieh.commons.comm.BasicClass{
  /** lab start **/

  // select append method start
/*
  private HashMap psreuse_pool = new HashMap();
  private Object psreuse_key = null;

  public void reusePreparedStatement(Object key)
  {
    Object psreuse_key = key;
  }
*/
  // select append method end


  // select append method start
  private boolean select_going = false;

  public void selectStart(){
    SQL.append("SELECT ");
  }

  public void selectColumn(Object column){
//    if(SQL.length() == 0) SQL.append("SELECT ");
    if(select_going) SQL.append(", ");
    SQL.append(column);
    select_going = true;
  }

  public void selectEnd(Object table){
    SQL.append(" FROM ").append(table);
//    SQL.append(" WHERE ");
    select_going = false; // 這是為了怕未來物件重置設計會遺漏，目前非必要。
  }

  public void selectCount(Object table){
    SQL.append("SELECT COUNT(*) FROM ").append(table);
  }

  // select append method end

  /** lab end **/

  public enum TYPE{FILE}

  // 應該要和 generateTableCache 整合
  public java.sql.ResultSet executeQuery(Connection conn)
  throws SQLException{
    String sql = SQL.toString();
    printSQL(sql);

    PreparedStatement ps = conn.prepareStatement(sql);
    runDesignate(ps);
    return ps.executeQuery();
  }

  public static final int FILE = 6;
  public static final int STR = 1;
  public static final int INT = 2;
  public static final int FLT = 3;
  public static final int DAT = 5;

  /**
   * database datetime 放空字串會寫回 1900/1/1 00:00:00，要規避這個問題則呼叫此常數則會
   * 自動放入 null 值。
   */
  public static final int SDT = 5;


  /**
   * where 條件之間連接的運算子，如 and, or...
   */
  public String OP1 = "AND";


  /**
   * 目前條件的邏輯條件，如 =, >, <
   */
  public String OP2 = "=";


  /**
   * SQL
   */
  public StringBuilder SQL = new StringBuilder();
//  private StringBuilder preSQL = new StringBuilder();


  // 紀錄使用者已經放入的所有需要處理 ? 的條件。
  private ArrayList<DesignatedData> designated = new ArrayList();

  /**
  * @param da DatabaseAdapter 資料庫連線介面。
  * @param table Table Name。
  * @throws SQLException
  * @return int 執行後異動資料庫的資料筆數。
  */
  public int excuteUpdate(
    DatabaseAdapter da,
    Object table
  ) throws SQLException {
    Connection c = da.getConnection();
    int r = 0;
    try {
      r = this.excuteUpdate(c, table);
    } catch (SQLException ex) { throw ex; }finally{ da.freeConnection(c); }
    return r;
  }


  /**
   * 執行 SQL Update。
   * @param conn Connection 資料庫連線資源。
   * @param table Table Name。
   * @throws SQLException
   * @return int 執行後異動資料庫的資料筆數。
   */
  public int excuteUpdate(
      Connection conn,
      Object table)
    throws SQLException {

//    preSQL.replace(0, preSQL.length(), SQL.toString());
    StringBuffer sql = new StringBuffer();
    sql.append("UPDATE ").append(table).append(" SET ");

    for(Iterator itr=designated.iterator(); itr.hasNext();){
      DesignatedData dd = (DesignatedData) itr.next();
      if(!dd.iswhere){
        sql.append(dd.column).append("=");
        if(dd.type != -1){
          sql.append("?");
        }else{
          sql.append(dd.value);
        }
        sql.append(", ");
      }
    }

    if(SQL.length() != 0)
      SQL.insert(0, " WHERE ");
    SQL.insert(0, sql.substring(0, sql.length()-2));

    return excuteModify(conn);
  }


  /**
   * 把用 setValue method 內設定的資料建構出 insert SQL 語法，並且執行。<p>
   * @param conn Connection 資料庫連線資源。
   * @param table Table Name。
   * @throws SQLException
   * @return int 執行後異動資料庫的資料筆數。
   */
  public int excuteInsert(
    Connection conn,
    Object table)
  throws SQLException {
    makeInsertSQL(table);
    return this.excuteModify(conn);
  }

  private void makeInsertSQL(Object table){
//    preSQL.replace(0, preSQL.length(), SQL.toString());
    StringBuffer q = new StringBuffer();
    SQL.append("INSERT INTO ").append(table).append("(");

    int l = designated.size() - 1;
    for (int i = 0; i < l; i++) {
      DesignatedData dd = (DesignatedData) designated.get(i);
      SQL.append(dd.column).append(", ");
      if (dd.type != -1) {
        q.append("?,");
      } else {
        q.append(dd.value).append(",");
      }
    }

    DesignatedData dd = (DesignatedData) designated.get(l);
    SQL.append(dd.column).append(") VALUES (");
    if (dd.type != -1) {
      q.append("?");
    } else {
      q.append(dd.value);
    }
    SQL.append(q.toString());
    SQL.append(")");
  }


  /**
   * 把用 setValue method 內設定的資料建構出 insert SQL 語法，並且執行。<p>
   * @param da DatabaseAdapter 資料庫連線介面。
   * @param table Table Name。
   * @throws SQLException
   * @return int 執行後異動資料庫的資料筆數。
   */
  public int excuteInsert(
    DatabaseAdapter da,
    Object table)
  throws SQLException
    {
    makeInsertSQL(table);
    return excuteModify(da);
  }


  /**
   * 執行 SQL Query 以外的語法。<p>
   * @param conn Connection 資料庫連線資源。
   * @throws SQLException
   * @return int 執行後異動資料庫的資料筆數。
   */
  public int excuteModify(Connection conn)
    throws SQLException
  {
    /*
    PreparedStatement ps = null;

    if(psreuse_key != null){
      if(!psreuse_pool.containsKey(psreuse_key)){
        String sql = SQL.toString();
        printSQL(sql);
        ps = conn.prepareStatement(sql);
        psreuse_pool.put(psreuse_key, ps);
      }else{
        ps = (PreparedStatement)psreuse_pool.get(psreuse_key);
        ps.clearParameters();
      }
    }else{
      String sql = SQL.toString();
      printSQL(sql);
      ps = conn.prepareStatement(sql);
    }
    */
    String sql = SQL.toString();
    printSQL(sql);
    PreparedStatement ps = conn.prepareStatement(sql);
    runDesignate(ps);
    int r = ps.executeUpdate();
    ps.close();
    ps = null;
    return r;
  }

  /**
   * 執行 SQL Query 以外的語法。<p>
   * @param da DatabaseAdapter 資料庫連線介面。
   * @throws SQLException
   * @return int 執行後異動資料庫的資料筆數。
   */
  public int excuteModify(DatabaseAdapter da)
  throws SQLException {
    Connection c = da.getConnection();
    int r = 0;
    try {
      r = this.excuteModify(c);
    } catch (SQLException ex) { throw ex; }finally{ da.freeConnection(c); }
    return r;
  }


  /**
   * 如果傳入的 value 有效的話，就以傳入的 column 加入並且紀錄
   * PreparedStatement ? 的參數。
   * @param type int value 所屬的資料庫欄位型態。
   * @param column 資料庫欄位名稱。
   * @param op2 第二個運算元。
   * @param value 值。
   */
  public void appendDesignatedWhere(int type, Object column, String op2, String value) {
    if (idv.jieh.commons.util.DataAnalysis.trimNull(value).length() > 0) {
      addDesignate(type, value, column.toString(), OP1, op2);
    }
  }


  /**
   * 不作判斷直接加上符合 <code>PreparedStatement</code>。
   * @param type int value 所屬的資料庫欄位型態。
   * @param column 資料庫欄位名稱。
   * @param op2 第二個運算元。
   * @param value 值。
   */
  public void appendWhere(int type, Object column, String op2, Object value) {
    addDesignate(type, value.toString(), column.toString(), OP1, op2);
  }


  /**
   * 不作判斷直接加上符合 <code>PreparedStatement</code>。
   * @param type int value 所屬的資料庫欄位型態。
   * @param value 值。
   * @param column 資料庫欄位名稱。
   */
  public void appendWhere(int type, Object column, Object value) {
    appendWhere(type, column, OP2, value);
  }


  /**
   * 直接建立 SQL 語法。
   * @param str SQL 語法。
   * @return StringBuilder 目前最後的 SQL 語法。
   */
  public StringBuilder append(Object str) {
    return SQL.append(str);
  }


  /**
   * 重設本物件還原初始值。
   */
  public void reset() {
    SQL.setLength(0);
    designated.clear();
    OP1 = "AND";
    OP2 = "=";
  }


//  /**
//   * 重複使用 SQL，把 SQL 還原到呼叫執行資料庫指令之前。
//   */
//  public void reuse(){
//    SQL.setLength(0);
//    SQL.replace(0, 0, preSQL.toString());
//  }


  /**
   * 如果傳入的 value 有效的話，就以傳入的 column 加入並且紀錄
   * PreparedStatement ? 的參數，並且以 SQL like 語法配合指定的 pattern 組合。
   * @param type int value 所屬的資料庫欄位型態。
   * @param value 值。
   * @param column 資料庫欄位名稱。
   * @param pattern String $val 代表要被置換 value 參數的位置。
   */
  public void appendDesignatedLikeWhere(int type, String value, Object column, String pattern) {
    if (idv.jieh.commons.util.DataAnalysis.trimNull(value).length() > 0) {
      value = idv.jieh.commons.util.DataAnalysis.replace(pattern, "$val", value);
      addDesignate(type, value, column.toString(), OP1, " LIKE ");
    }
  }


  /**
   * 如果傳入的 value 有效的話，就以傳入的 column 加入並且紀錄
   * PreparedStatement ? 的參數。
   * @param type int value 所屬的資料庫欄位型態。
   * @param column 資料庫欄位名稱。
   * @param value 值。
   */
  public void appendDesignatedWhere(int type, Object column, String value) {
    if (idv.jieh.commons.util.DataAnalysis.trimNull(value).length() > 0) {
      addDesignate(type, value, column.toString(), OP1, OP2);
    }
  }


  public void appendDesignatedWhere(int type, Object column, boolean in, String... values) {
    if (values.length == 0)  return;

    switch(type){
      case STR:
      case INT:
      case FLT:
        break;
      default:
        throw new IllegalArgumentException("您所使用的資料型態不適合透過此方法進行 SQL IN 的語法！");
    }

    // 只用 SQL.length()>0 判斷太潦草
    if(SQL.length()>0 && !checkWhere()) SQL.append(" ").append(OP1);
    SQL.append(" ").append(column);
    if(!in) SQL.append(" not");
    SQL.append(" in(");
    for(int i=0, l=values.length; i<l; i++){
      if(type == STR) SQL.append("'");
      SQL.append(values[i]);
      SQL.append(type == STR?"',":",");
    }
    int l = SQL.length();
    SQL.lastIndexOf(",");
    SQL.replace(l-1, l, ")");
  }


  // 將需要作 ? 處理的資料放到 designated 陣列中
  private void addDesignate(int t, String v, String c, String o1, String o2) {
    setDesignate(t, c, v, true);

    // 可思考 and 置後的概念 如 : where pco_did = ? and pco_engno = ? and...
    if (SQL.length() > 0 && !checkWhere())
      SQL.append(" ").append(o1);
    SQL.append(" ").append(c).append(o2).append("?");
  }


  private void setDesignate(int t, String c, String v, boolean iswhere){
    DesignatedData dd = new DesignatedData();
    dd.iswhere = iswhere;
    dd.column = c;
    dd.type = t;
    dd.value = idv.jieh.commons.util.DataAnalysis.trimNull(v);
    designated.add(dd);
  }


  /**
   * 放入 SQL where 以外的資料，並以 PreparedStatement ? 建構語法。<p>
   * @param type value 所屬的資料庫欄位型態。
   * @param column 資料庫欄位名稱。
   * @param value 值。
   */
  public void setValue(int type, Object column, String value){
    setDesignate(type, column.toString(), value, false);
  }


  /**
   * 放入 SQL where 以外的資料，並直接用將 value 替代對應的位置。
   * @param column 資料庫欄位名稱。
   * @param value 值。
   */
  public void setValue(Object column, String value){
    setValue(-1, column, value);
  }


  /**
   * 建構 <code>TableCache</code>。<p>
   * @param conn Connection 資料庫連線資源。
   * @param tc TableCache 資料庫離線物件。
   * @throws SQLException
   */
  public void generateTableCache(
    Connection conn,
    TableCache tc)
  throws SQLException {
    ResultSet rs = this.executeQuery(conn);
    tc.generate(rs);

    Statement ps = rs.getStatement();
    rs.close();
    ps.close();
  }


  /**
   * 建構 <code>TableCache</code>。<p>
   * @param da DatabaseAdapter 資料庫連線介面。
   * @param tc TableCache 資料庫離線物件。
   * @throws SQLException
   */
  public void generateTableCache(
    DatabaseAdapter da,
    TableCache tc)
  throws SQLException {
    Connection c = da.getConnection();
    try {
      generateTableCache(c, tc);
    } catch (SQLException ex) { throw ex; }finally{ da.freeConnection(c); }
  }

  /**
   * @deprecated 這方法完全沒用，請勿使用！
   */
  private void printSQL(){
    printSQL(SQL.toString());
  }

  // 顯示組合值的 SQL 結果
  protected void printSQL(String s){
    if(!dbr.isDebug())return;
    StringBuilder sql = new StringBuilder(s);
    for(Iterator itr=designated.iterator(); itr.hasNext();){
      DesignatedData dd = (DesignatedData) itr.next();
      if (dd.type != -1) {
        String value = dd.value.toString();
        switch(dd.type){
          case SDT :
          case INT :
            value = value.length()==0?"null":value;
            break;
          case STR :
            value = '\'' + value + '\'';
            break;
        }
        int idx = sql.toString().indexOf("?");
        if(idx != -1)
          sql.replace(idx, ++idx, value==null?"null":value);
      }
    }
    dbr.debug("printSQL", sql.toString());
  }


  // 2004-07-18 jieh
  // 把存放在 DesignatedData 的資料逐一取出判斷是否需要呼叫 PreparedStatement
  // 的 setter method。
  protected void runDesignate(java.sql.PreparedStatement ps)
    throws SQLException {

    int count = 1;
    for(Iterator itr=designated.iterator(); itr.hasNext();){
      DesignatedData dd = (DesignatedData) itr.next();
      if (dd.type != -1) {
        try {
          set(count, dd, ps);
        } catch (Exception ex){
          dbr.debug("runDesignate", dd.column + " is error...... : " + ex.getMessage());
        }
        count++;
      }
    }
  }


  // for PreparedStatement 從傳入的 DesignatedData 判斷呼叫哪種型態的 setter method。
  private void set(int i, DesignatedData dd, java.sql.PreparedStatement ps)
      throws SQLException {
    String val = dd.value.toString();
    switch (dd.type) {
      case STR:
        ps.setString(i, val);
        break;
      case INT:
//        int numi = val.length()==0?0:Integer.parseInt(val);
        if(val.length() == 0)
          ps.setNull(i, Types.NULL);
        else
          ps.setInt(i, Integer.parseInt(val));
        break;
        // add 2004-07-08 jieh
      case FLT:
//        float numf = val.length()==0?0:Float.parseFloat(val);
//        ps.setFloat(i, numf);
       if(val.length() == 0)
          ps.setNull(i, Types.NULL);
        else
//          ps.setInt(i, Integer.parseInt(val));
          ps.setBigDecimal(i, new java.math.BigDecimal(val));
        break;
        // add 2004-07-16 jieh
      case SDT:
        ps.setString(i, val.length() == 0 ? null : val);
        break;
        // add 2005-12-13 jieh
      case FILE:
        ps.setBinaryStream(i, (java.io.InputStream)dd.value, dd.file_vavilable);
        break;
    }
  }

  public void setFile(Object column, java.io.InputStream file)
  throws java.io.IOException{
    DesignatedData dd = new DesignatedData();
    dd.column = column.toString();
    dd.iswhere = false;
    // 要改 enum
    dd.type = FILE;
    dd.value = file;
    dd.file_vavilable = file.available();
    designated.add(dd);
  }

  private class DesignatedData {
    boolean valid;
    boolean iswhere;
    int type;
    TYPE enum_type;
    String column;
    Object value;
    int file_vavilable;
  }

  private boolean checkWhere(){
    return SQL.toString().trim().toUpperCase().endsWith("WHERE");
  }

  public static void main(String[] args){
    Vector v = new Vector();
    SQLMaker sm = new SQLMaker();
    String[] s = {"aa", "bb"};
    String[] empty_str_array = {""};
//    sm.appendDesignatedWhere(0, "", true, v.size()==0?empty_str_array:s);
    sm.setValue(INT, "TEST", "");
    sm.setValue(INT, "TEST", "0");
    sm.appendDesignatedWhere(SQLMaker.INT, "test", "0");
    sm.printSQL();
    System.out.println(sm.SQL);
  }

} // end class SQLMaker



// 2005-11-06 jieh - 要大翻改 SQLMaker 把 type 改用 enum 的方法，
// uploadFile 要整合到 excuteInsert，不過一般資料和檔案進資料庫好像要分開處理，要測試。
//  public boolean uploadFile(
//    Connection conn,
//    Object table)
//	throws java.io.IOException, SQLException{
//    makeInsertSQL(table);
//    java.sql.PreparedStatement ps = conn.prepareStatement(SQL.toString());
//    int i = 1;
//    for(DesignatedData dd:designated){
//      java.io.InputStream file = (java.io.InputStream)dd.value;
//      ps.setBinaryStream(i++, file, file.available());
//    }
//    return ps.execute();
//  }


// 2005-11-06 jieh
//public void setValue(TYPE type, Object column, Object value){
//  DesignatedData dd = new DesignatedData();
//  dd.column = column.toString();
//  dd.iswhere = false;
//  dd.enum_type = type;
//  dd.value = value;
//  designated.add(dd);
//  }

